package com.uxsino.simo.collector.connections;

import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.uxsino.commons.utils.StringUtils;
import com.uxsino.simo.connections.AbstractConnection;
import com.uxsino.simo.connections.exception.SimoConnectionException;
import com.uxsino.simo.connections.exception.SimoQueryException;
import com.uxsino.simo.connections.target.HttpTarget;

/**
 * This class right now only deals with authentication by (userId, token) pair.
 * Fields in HttpTarget needs to be set via Entity table or QueryTemplates for
 * this to work. This is built with the existing example of 北统三期 as template,
 * and generalized enough to allow most queries described in their API to work
 * out, IF the configuration of the entity as well as query XML's are done in a
 * compatible way. <br />
 * Look at the docs on each method to see the details, especially
 * {@code connect()} and {@code buildCmd()}.
 * 
 * @author once
 *
 */
public class HuaweiCloudConnection extends AbstractConnection<HttpTarget> {
    private static Logger logger = LoggerFactory.getLogger(HuaweiCloudConnection.class);

    /**
     * For uses of these constants, see method {@code buildCmd()}.
     */
    private static final String URL_KEY = "relativeURL";

    private static final String VDC_URL_KEY = "vdcURL";

    private static final String REQUEST_KEY = "requestEntity";

    private static final String METHOD_KEY = "method";

    private static final String VARIABLES_KEY = "uriVariables";

    private static final String REQUEST_BODY_KEY = "requestBody";

    private static final String HEADER_INFO_KEY = "headerInfo";

    private HttpTarget target;

    private String baseUrl;

    // private HttpHeaders cmdHeaders = new HttpHeaders();

    /**
     * 
     * used to save token id info
     *
     */
    private static class TokenEntry {

        public String userId;

        public String tokenId;

        public long lastAccessMillis;

        public long loginMillis;
    }

    /**
     * token id got when login
     */
    private static ConcurrentMap<HttpTarget, TokenEntry> tokenIds = new ConcurrentHashMap<>();

    /**
     * Take the properties set in HttpTarget, specifically: <br />
     * {@code contentType, clientTP, userNameString, passwordString, connectUrl}
     * for sending in connection request. <br />
     * {@code tokenString, userIdString} for getting the (userId, token) in the
     * response. <br />
     * It is assumed for now that the response body is a JSON string, with the
     * fields with name given by {@code tokenString, userIdString}. <br />
     * This then sets the fields {@code userId, token} in the HttpTarget.
     */
    @Override
    public int connect(HttpTarget _target) {
        super.connect(_target);
        connected = false;
        state = 0;
        /**
         * supposedly -1 is a bad number.
         */
        if (null == _target) {
            return -1;
        }

        target = _target;

        createBaseUrl();
        HttpHeaders headers = initCmdHeaders(null);

        HttpEntity<String> connectRequest = new HttpEntity<String>(headers);

        RestTemplate restTemplate = RestTemplateFactory.createRestTemplate(target.getConnectProtocol());
        try {
            HttpStatus statusCode = restTemplate.postForEntity(baseUrl, connectRequest, String.class).getStatusCode();
            connected = statusCode.is2xxSuccessful() || statusCode.is3xxRedirection();
        } catch (Exception e) {
            logger.error("Error connecting to {}.", baseUrl, e);
            return state;
        }

        TokenEntry te = getTokenEntry(_target);
        if (te == null) {
            te = login();
        }

        if (te == null) {
            logger.error("can not login: {}", _target);
            connected = false;
        } else {
            connected = true;
            state = 1;
        }
        return state;
    }

    /**
     * This creates the base URL for the REST session.
     * <br />
     * The result of this method appended with the target.loginRelativeUrl gives the URL of
     * obtaining authentication information.
     * 
     * @param target
     * @return
     */
    private void createBaseUrl() {
        try {
            /*            URI baseURI = new URI(HttpTarget.HTTPS, null, target.host, target.port, target.basePath, null, null);*/
            URI baseURI = new URI(target.connectProtocol, null, target.host, target.port, target.basePath, null, null);
            baseUrl = baseURI.toString();
        } catch (Exception e) {
            logger.error("URI formatting error for {}, {}, {}", target.host, target.port, target.basePath);
        }
    }

    /**
     * Build the common headers
     */
    private HttpHeaders initCmdHeaders(TokenEntry te) {
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", target.contentType);

        if (te != null) {
            headers.add("X-Auth-User-ID", te.userId);
            headers.add("X-HW-Cloud-Auth-Token", te.tokenId);
        }
        if (null != target.userAgent) {
            headers.add("User-Agent", target.userAgent);
        }

        return headers;
    }

    /**
     * This method returns the String extracted from the {@link ResponseEntity}.
     * If desired, more information can be passed out. <br />
     * The parameter {@code cmd} is the one built by {@code buildCmd}, which
     * should be a {@link HashMap}, where the keys are given in the private
     * variable of this class.
     * @throws SimoConnectionException 
     */
    @Override
    @SuppressWarnings("unchecked")
    public Object execCmd(Object cmdPattern) throws SimoConnectionException {
        Map<String, Object> cmdMap = (HashMap<String, Object>) cmdPattern;
        RestTemplate restTemplate = RestTemplateFactory.createRestTemplate(target.getConnectProtocol());
        String result = null;

        ResponseEntity<String> cmdResponse;

        // set current vdc
        try {
            if (cmdMap.containsKey(VDC_URL_KEY)) {
                TokenEntry te = target != null ? tokenIds.get(target) : null;

                HttpHeaders headers = initCmdHeaders(te);
                cmdResponse = restTemplate.exchange((String) cmdMap.get(VDC_URL_KEY), HttpMethod.PUT,
                    new HttpEntity<String>(headers), String.class);
                logger.debug("current vdc set:{}", cmdResponse.toString());
            }
        } catch (RestClientException e) {
            logger.error("Error switching vdc:{} {}", cmdMap.get(VDC_URL_KEY), e);
            throw new SimoConnectionException(e);
        }
        try {
            /**
             * Could have used {@link RequestEntity} but that thing does not
             * seem to allow {@code uriVariables}. The code below should work
             * even when the last entry is {@code null}, since the method is
             * overloaded without the last parameter ....
             */

            logger.debug("cmdMap:{}", cmdMap);

            cmdResponse = restTemplate.exchange((String) cmdMap.get(URL_KEY), (HttpMethod) cmdMap.get(METHOD_KEY),
                (HttpEntity<String>) cmdMap.get(REQUEST_KEY), String.class,
                (HashMap<String, ?>) cmdMap.get(VARIABLES_KEY));

            if (cmdResponse.hasBody()) {
                result = cmdResponse.getBody();
                logger.debug("*** result: {}", result);
            }
        } catch (RestClientException e) {
            logger.error("Error excuting command {} with parameter {} at {}.", cmdMap.get(METHOD_KEY),
                cmdMap.get(VARIABLES_KEY), cmdMap.get(URL_KEY), e);
            throw new SimoConnectionException(e);
        }
        // return wrapParameters(result, cmdPattern.getParameters());
        return result;
    }

    /**
     * The second parameter {@code args} is not used.
     * <br /> 
     * The {@code cmdString} is a JSON string, containing the following fields:
     * <br />
     * {@code "relativeURL"}: which is the relative url with respect to the baseUrl constructed in 
     * {@code createBaseUrl()}.
     * <br />
     * *{@code "vdcURL"}: to set the current vdc id which is required to get further info of a vdc
     * {@code "uriVariables"}: which is itself a JSON string, containing (key, value) pairs describing
     * the query to be passed
     * <br />
     * {@code "headerInfo"}: which is itself a JSON string, containing (key, value) pairs describing 
     * the things that should go into the header. One should really use multiMap, but well ...
     * <br />
     * {@code "method"}: containing the HTTPmethod, e.g. GET, POST, etc.
     * <br />
     * {@code "requestBody"}: which is string, containing the body of the request. No extra processing done.
     * It should be the same as the parameter after -d in a call using curl.
     * @throws SimoQueryException 
     */
    @Override
    public Object buildCmd(String cmdPattern, Map<String, String> args) throws SimoQueryException {

        logger.debug("build cmd on :{}", cmdPattern);
        TokenEntry te = target != null ? tokenIds.get(target) : null;

        JSONObject cmdObject;
        try {
            cmdObject = JSON.parseObject(cmdPattern);
        } catch (Exception e) {
            logger.error("error parsing cmd pattern into json. {}\n{}", cmdPattern, e);
            throw new SimoQueryException(e);
        }

        String vdcUrl = cmdObject.getString(VDC_URL_KEY);
        String relativeUrl = cmdObject.getString(URL_KEY);

        if (te != null) {
            logger.debug("token found. key:{}", target);
            relativeUrl = relativeUrl.replace("##userId##", te.userId);
            relativeUrl = relativeUrl.replace("##tokenId##", te.tokenId);
            if (vdcUrl != null)
                vdcUrl = vdcUrl.replace("##userId##", te.userId);
        } else {
            logger.debug("token not found. key:{}", target);
        }
        String requestBody = cmdObject.getString(REQUEST_BODY_KEY);
        String method = cmdObject.getString(METHOD_KEY);

        HashMap<String, String> headerInfo = JSONStringToHashMap(cmdObject.getString(HEADER_INFO_KEY));
        HashMap<String, String> uriVariable = JSONStringToHashMap(cmdObject.getString(VARIABLES_KEY));

        HttpHeaders headers = initCmdHeaders(te);

        String resultUrl = baseUrl + relativeUrl;

        Map<String, Object> result = new HashMap<>();

        if (null != method) {
            result.put(METHOD_KEY, HttpMethod.valueOf(method));
        }
        if (null != headerInfo && !headerInfo.isEmpty()) {
            for (Map.Entry<String, String> entry : headerInfo.entrySet()) {
                headers.add(entry.getKey(), entry.getValue());
            }
        }
        result.put(VARIABLES_KEY, uriVariable);

        if (null != uriVariable && !uriVariable.isEmpty()) {
            // define the template to expand the variables in URL
            resultUrl += "?";
            int count = 0;

            for (Map.Entry<String, String> varEntry : uriVariable.entrySet()) {
                if (count > 0) {
                    resultUrl += "&";
                }
                resultUrl += varEntry.getKey() + "=" + varEntry.getValue();

                count++;
            }
        }

        result.put(URL_KEY, resultUrl);
        if (StringUtils.isNotEmpty(vdcUrl)) {
            result.put(VDC_URL_KEY, baseUrl + vdcUrl);
        }
        HttpEntity<String> cmdRequest = null;
        if (null != requestBody && !requestBody.isEmpty()) {
            cmdRequest = new HttpEntity<String>(requestBody, headers);
        } else {
            cmdRequest = new HttpEntity<String>(headers);
        }
        result.put(REQUEST_KEY, cmdRequest);

        return result;
    }

    private TokenEntry getTokenEntry(HttpTarget target) {

        TokenEntry entry = tokenIds.get(target);

        if (null == entry)
            return null;
        long now = System.currentTimeMillis();

        if ((now - entry.lastAccessMillis) / 1000 > target.tokenTimeoutSeconds) {
            // check expiration
            tokenIds.remove(target);
            return null;
        }
        return entry;
    }

    private static Map<String, String> emptyArgs = new HashMap<>();

    private TokenEntry login() {
        String cmd = "{\"relativeURL\":\"/tokens\", \"headerInfo\": \"{\\\"Host\\\": \\\"" + target.host
                + "\\\"}\", \"method\": \"POST\", \"requestBody\": \"{\\\"userName\\\":\\\"" + target.getUsername()
                + "\\\", \\\"password\\\": \\\"" + target.getPassword() + "\\\"}\"}";

        logger.debug("login cmd:{}", cmd);
        String cmdResult = null;
        try {
            @SuppressWarnings("unchecked")
            Map<String, Object> pattern = (Map<String, Object>) buildCmd(cmd, emptyArgs);
            logger.debug("login cmd pattern:{}", pattern);
            if (pattern == null) {
                logger.error("error get login config. abort");
                return null;
            }
            cmdResult = execCmd(pattern).toString();
        } catch (SimoConnectionException | SimoQueryException e) {
            logger.error("execute command failed due to {}", e);
        }

        if (cmdResult == null) {
            logger.error("login error, return null.");
            return null;
        }

        logger.debug("login return: {}", cmdResult);
        ObjectMapper mapper = new ObjectMapper();

        try {
            JsonNode t = mapper.readTree(cmdResult);

            JsonNode node = t.get("tokenId");

            if (node == null) {
                return null;
            }

            TokenEntry entry = new TokenEntry();
            entry.tokenId = node.asText();
            entry.loginMillis = System.currentTimeMillis();
            entry.lastAccessMillis = entry.loginMillis;

            node = t.get("userId");

            if (node == null) {
                return null;
            }
            entry.userId = node.asText();
            logger.debug("logged in. key:{}", target);
            tokenIds.put(target, entry);
            return entry;
        } catch (IOException e) {
            logger.error("error login. {}", e);
            return null;
        }
    }

    /**
     * RESTful connection should not have anything to close.
     */
    @Override
    public int close() {
        connected = false;
        super.close();
        return 0;
    }

    /**
     * private helper
     * make the map contained in the input JSON{@link String} {@code jsonString} into a {@link HashMap}
     * @param jsonObj
     * @return
     */
    private HashMap<String, String> JSONStringToHashMap(String jsonString) {
        HashMap<String, String> result = new HashMap<>();
        if (null != jsonString) {
            JSONObject jsonObj = JSON.parseObject(jsonString);
            for (Map.Entry<String, Object> entry : jsonObj.entrySet()) {
                result.put(entry.getKey(), (String) entry.getValue());
            }
        }
        return result;
    }
}
